Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit f924086198dece6b3bb5d5f772295b3674d14241


Parents : 6abb31e
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Date : 2026-04-27T00:06:33+02:00

Refactored rnsh to use argparse

Changes
Diff

diff --git a/RNS/Utilities/rnsh/args.py b/RNS/Utilities/rnsh/args.py
new file mode 100644
index 00000000..a9ace4c1
--- /dev/null
+++ b/RNS/Utilities/rnsh/args.py
@@ -0,0 +1,59 @@
+import argparse
+import sys
+
+from RNS.Utilities.rnsh._version import __version__ as __rnsh_version__
+from RNS._version import __version__
+
+DEFAULT_SERVICE_NAME = "default"
+
+def setup_argument_parser():
+ parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter)
+
+ # Common options
+ parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
+ parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str)
+ parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
+ parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
+ parser.add_argument("-p", "--print-identity", action="store_true", default=False, help="print identity and destination info and exit")
+ parser.add_argument("--version", action="version", version="rnsh {rv} (protocol {pv})".format(rv=__version__, pv=__rnsh_version__))
+
+ # Listener options
+ parser.add_argument("-l", "--listen", action="store_true", default=False, help="listen (server) mode; any command specified after -- will be used as the default command when the initiator does not provide one or when remote command execution is disabled; if no command is specified, the default shell of the user running rnsh will be used")
+ parser.add_argument("-s", "--service", action="store", default=None, help="service name for identity file if not the default", type=str)
+ parser.add_argument("-b", "--announce",action="store", default=None,help="announce on startup and every PERIOD seconds; specify 0 to announce on startup only",metavar="PERIOD", type=int)
+ parser.add_argument("-a", "--allowed", action="append", default=None, metavar="HASH", type=str, help="allow this identity to connect (may be specified multiple times); allowed identities can also be specified in ~/.rnsh/allowed_identities or ~/.config/rnsh/allowed_identities, one hash per line")
+ parser.add_argument("-n", "--no-auth", action="store_true", default=False, help="disable authentication (allow any identity to connect)")
+ parser.add_argument("-A", "--remote-command-as-args", action="store_true", default=False, help="concatenate remote command to the argument list of the default program or shell")
+ parser.add_argument("-C", "--no-remote-command", action="store_true", default=False, help="disable executing command lines received from the remote initiator")
+
+ # Initiator options
+ parser.add_argument("-N", "--no-id", action="store_true", default=False, help="disable identity announcement on connect")
+ parser.add_argument("-m", "--mirror", action="store_true", default=False, help="return with the exit code of the remote process")
+ parser.add_argument("-w", "--timeout", action="store", default=None, help="connect and request timeout in seconds", metavar="SECONDS", type=float)
+
+ parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination to connect to", type=str)
+
+ return parser
+
+
+def parse_arguments(argv=None):
+ if argv is None: argv = sys.argv[1:]
+
+ # Split at -- to separate rnsh options from the command to execute.
+ # Everything before -- (or the entire argv if no --) goes to argparse.
+ # Everything after -- becomes the command list.
+ try:
+ split_idx = argv.index("--")
+ rnsh_argv = argv[:split_idx]
+ command = argv[split_idx + 1:]
+ except ValueError:
+ rnsh_argv = argv
+ command = []
+
+ parser = setup_argument_parser()
+ args = parser.parse_args(rnsh_argv)
+ args.command = command
+
+ if args.listen and not args.service: args.service = DEFAULT_SERVICE_NAME
+
+ return args, parser

diff --git a/RNS/Utilities/rnsh/initiator.py b/RNS/Utilities/rnsh/initiator.py
index 96df2904..c83a442c 100644
--- a/RNS/Utilities/rnsh/initiator.py
+++ b/RNS/Utilities/rnsh/initiator.py
@@ -39,18 +39,18 @@ import time
import tty
from typing import Callable, TypeVar
import RNS
-import rnsh.exception as exception
-import rnsh.process as process
-import rnsh.retry as retry
-import rnsh.session as session
+import RNS.Utilities.rnsh.exception as exception
+import RNS.Utilities.rnsh.process as process
+import RNS.Utilities.rnsh.retry as retry
+import RNS.Utilities.rnsh.session as session
import re
import contextlib
-import rnsh.args
+
import pwd
import bz2
-import rnsh.protocol as protocol
-import rnsh.helpers as helpers
-import rnsh.rnsh
+import RNS.Utilities.rnsh.protocol as protocol
+import RNS.Utilities.rnsh.helpers as helpers
+import RNS.Utilities.rnsh.rnsh as rnsh
_identity = None
_reticulum = None
@@ -154,7 +154,7 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE)
if _identity is None:
- _identity = rnsh.rnsh.prepare_identity(identitypath)
+ _identity = rnsh.prepare_identity(identitypath)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
@@ -169,7 +169,7 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0
listener_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
- rnsh.rnsh.APP_NAME
+ rnsh.APP_NAME
)
if _link is None or _link.status == RNS.Link.PENDING:

diff --git a/RNS/Utilities/rnsh/listener.py b/RNS/Utilities/rnsh/listener.py
index fa89741e..5fb49917 100644
--- a/RNS/Utilities/rnsh/listener.py
+++ b/RNS/Utilities/rnsh/listener.py
@@ -36,17 +36,17 @@ import time
import tty
from typing import Callable, TypeVar
import RNS
-import rnsh.exception as exception
-import rnsh.process as process
-import rnsh.retry as retry
-import rnsh.session as session
+import RNS.Utilities.rnsh.exception as exception
+import RNS.Utilities.rnsh.process as process
+import RNS.Utilities.rnsh.retry as retry
+import RNS.Utilities.rnsh.session as session
import re
import contextlib
-import rnsh.args
+
import pwd
-import rnsh.protocol as protocol
-import rnsh.helpers as helpers
-import rnsh.rnsh
+import RNS.Utilities.rnsh.protocol as protocol
+import RNS.Utilities.rnsh.helpers as helpers
+import RNS.Utilities.rnsh.rnsh as rnsh
_identity = None
@@ -123,8 +123,8 @@ async def listen(configdir, rnsconfigdir, command, identitypath=None, service_na
# More -v should increase verbosity (higher RNS.loglevel); -q should decrease it
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel)
- _identity = rnsh.rnsh.prepare_identity(identitypath, service_name)
- _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.rnsh.APP_NAME)
+ _identity = rnsh.prepare_identity(identitypath, service_name)
+ _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME)
RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE)

diff --git a/RNS/Utilities/rnsh/process.py b/RNS/Utilities/rnsh/process.py
index 66e99764..e0ebce49 100644
--- a/RNS/Utilities/rnsh/process.py
+++ b/RNS/Utilities/rnsh/process.py
@@ -40,7 +40,7 @@ import types
import typing
import RNS
-import rnsh.exception as exception
+import RNS.Utilities.rnsh.exception as exception
CTRL_C = "\x03".encode("utf-8")
CTRL_D = "\x04".encode("utf-8")

diff --git a/RNS/Utilities/rnsh/protocol.py b/RNS/Utilities/rnsh/protocol.py
index 664a8337..3ed20ccb 100644
--- a/RNS/Utilities/rnsh/protocol.py
+++ b/RNS/Utilities/rnsh/protocol.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import RNS
from RNS.vendor import umsgpack
from RNS.Buffer import StreamDataMessage as RNSStreamDataMessage
-import rnsh.retry
+import RNS.Utilities.rnsh.retry
import abc
import contextlib
import struct
@@ -77,7 +77,7 @@ class VersionInfoMessage(RNS.MessageBase):
def __init__(self, sw_version: str = None):
super().__init__()
- self.sw_version = sw_version or rnsh.__version__
+ self.sw_version = sw_version or RNS.Utilities.rnsh.__version__
self.protocol_version = PROTOCOL_VERSION
def pack(self) -> bytes: return umsgpack.packb((self.sw_version, self.protocol_version))

diff --git a/RNS/Utilities/rnsh/retry.py b/RNS/Utilities/rnsh/retry.py
index b761a102..7bb6b4ed 100644
--- a/RNS/Utilities/rnsh/retry.py
+++ b/RNS/Utilities/rnsh/retry.py
@@ -1,6 +1,10 @@
-# MIT License
+# Based on the original rnsh program by Aaron Heise (@acehoss)
+# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
+# This version of rnsh is included in RNS under the Reticulum License
#
-# Copyright (c) 2023 Aaron Heise
+# Reticulum License
+#
+# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +13,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
+# - The Software shall not be used in any kind of system which includes amongst
+# its functions the ability to purposefully do harm to human beings.
+#
+# - The Software shall not be used, directly or indirectly, in the creation of
+# an artificial intelligence, machine learning or language model training
+# dataset, including but not limited to any use that contributes to the
+# training or development of such a model or algorithm.
+#
+# - The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -23,7 +35,7 @@
import asyncio
import threading
import time
-import rnsh.exception as exception
+import RNS.Utilities.rnsh.exception as exception
from typing import Callable
from contextlib import AbstractContextManager
import types

diff --git a/RNS/Utilities/rnsh/rnsh.py b/RNS/Utilities/rnsh/rnsh.py
index f3a3f79a..b1ccbde4 100644
--- a/RNS/Utilities/rnsh/rnsh.py
+++ b/RNS/Utilities/rnsh/rnsh.py
@@ -32,12 +32,13 @@ import os
import sys
import RNS
-import rnsh.process as process
-import rnsh.session as session
-import rnsh.args
-import rnsh.loop
-import rnsh.listener as listener
-import rnsh.initiator as initiator
+import RNS.Utilities.rnsh.process as process
+import RNS.Utilities.rnsh.session as session
+import RNS.Utilities.rnsh.args
+import RNS.Utilities.rnsh.loop
+import RNS.Utilities.rnsh.listener as listener
+import RNS.Utilities.rnsh.initiator as initiator
+from RNS.Utilities.rnsh.args import parse_arguments
APP_NAME = "rnsh"
loop: asyncio.AbstractEventLoop | None = None
@@ -91,13 +92,13 @@ def ensure_config_directory():
async def _rnsh_cli_main():
global verbose_set
- args = rnsh.args.Args(sys.argv)
+ args, parser = parse_arguments()
verbose_set = args.verbose > 0
configdir = ensure_config_directory()
if args.print_identity:
- print_identity(args.config, args.identity, args.service_name, args.listen)
+ print_identity(args.config, args.identity, args.service, args.listen)
return 0
if args.listen:
@@ -110,17 +111,17 @@ async def _rnsh_cli_main():
await listener.listen(configdir=configdir,
rnsconfigdir=args.config,
- command=args.command_line,
+ command=args.command,
identitypath=args.identity,
- service_name=args.service_name,
+ service_name=args.service,
verbosity=args.verbose,
quietness=args.quiet,
- allowed=args.allowed,
+ allowed=args.allowed or [],
allowed_file=allowed_file,
disable_auth=args.no_auth,
announce_period=args.announce,
- no_remote_command=args.no_remote_cmd,
- remote_cmd_as_args=args.remote_cmd_as_args)
+ no_remote_command=args.no_remote_command,
+ remote_cmd_as_args=args.remote_command_as_args)
return 0
if args.destination is not None:
@@ -132,12 +133,12 @@ async def _rnsh_cli_main():
noid=args.no_id,
destination=args.destination,
timeout=args.timeout,
- command=args.command_line
+ command=args.command
)
return return_code if args.mirror else 0
else:
print("")
- print(rnsh.args.usage)
+ parser.print_help()
print("")
return 1

diff --git a/RNS/Utilities/rnsh/session.py b/RNS/Utilities/rnsh/session.py
index 0c494a64..82b8dfa0 100644
--- a/RNS/Utilities/rnsh/session.py
+++ b/RNS/Utilities/rnsh/session.py
@@ -1,11 +1,11 @@
from __future__ import annotations
import contextlib
import functools
-import rnsh.exception as exception
import asyncio
-import rnsh.process as process
-import rnsh.helpers as helpers
-import rnsh.protocol as protocol
+import RNS.Utilities.rnsh.exception as exception
+import RNS.Utilities.rnsh.process as process
+import RNS.Utilities.rnsh.helpers as helpers
+import RNS.Utilities.rnsh.protocol as protocol
import enum
from typing import TypeVar, Generic, Callable, List
from abc import abstractmethod, ABC


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────